"""
认证服务
包含:
1. AuthService - 认证服务类，提供登录、注册、令牌刷新等功能
2. 支持多种认证方式：密码、邮箱、手机号、GitHub等
3. 包含登录尝试次数限制功能
4. 支持令牌管理和黑名单
5. 支持验证码功能
6. 支持密码重置
7. 支持账号绑定/解绑
"""

from base64 import b64encode
from enum import IntEnum
import random
import time
from logging import getLogger
from typing import Any, Dict, Optional, Tuple, Protocol
from uuid import uuid4
from abc import abstractmethod

from app.core.config import settings
from app.core.exceptions import APIException
from app.core.i18n import t
from app.core.redis_conn import redis_helper
from app.core.security import (
    add_token_to_blacklist,
    create_access_token,
    create_refresh_token,
    get_password_hash,
    verify_password,
    verify_refresh_token,
)
from app.models.auth_account_model import AuthAccountManager, AuthProviderEnum, UserAuthAccountModel
from app.models.user_model import UserModel
from app.utils.captcha_helper import CaptchaConfig, GIFCaptchaGenerator, PNGCaptchaGenerator
from app.utils.email_helper import email_helper
from app.utils.sms_helper import sms_helper

logger = getLogger(__name__)


class CaptchaTypeIntEnum(IntEnum):
    GIF = 1
    PNG = 2


class LoginStrategy(Protocol):
    """登录策略接口"""

    @abstractmethod
    async def validate_credentials(self, account: str, **kwargs) -> UserAuthAccountModel:
        """验证凭证"""
        pass


class PasswordLoginStrategy:
    """密码登录策略"""

    async def validate_credentials(self, account: str, **kwargs) -> UserAuthAccountModel:
        password = kwargs.get("password")
        if not password:
            raise APIException(message=t("error.auth.password_required"))

        auth_account = await AuthAccountManager.login_by_password(account, password)
        if not auth_account:
            raise APIException(message=t("error.auth.invalid_password"))

        return auth_account


class EmailLoginStrategy:
    """邮箱登录策略"""

    async def validate_credentials(self, account: str, **kwargs) -> UserAuthAccountModel:
        code = kwargs.get("code")
        if not code:
            raise APIException(message=t("error.auth.code_required"))

        if not await AuthService._verify_captcha(f"email_code:{account}", code):
            raise APIException(message=t("error.auth.invalid_code"))

        auth_account = await UserAuthAccountModel.get_by_identifier(AuthProviderEnum.EMAIL, account)
        if not auth_account:
            raise APIException(message=t("error.auth.account_not_found"))

        return auth_account


class PhoneLoginStrategy:
    """手机登录策略"""

    async def validate_credentials(self, account: str, **kwargs) -> UserAuthAccountModel:
        code = kwargs.get("code")
        if not code:
            raise APIException(message=t("error.auth.code_required"))

        if not await AuthService._verify_captcha(f"phone_code:{account}", code):
            raise APIException(message=t("error.auth.invalid_code"))

        auth_account = await UserAuthAccountModel.get_by_identifier(AuthProviderEnum.PHONE, account)
        if not auth_account:
            raise APIException(message=t("error.auth.account_not_found"))

        return auth_account


class RegisterStrategy(Protocol):
    """注册策略接口"""

    @abstractmethod
    async def validate_credentials(self, account: str, **kwargs) -> Tuple[UserModel, UserAuthAccountModel]:
        """
        验证凭证并创建用户
        :param account: 账号
        :param kwargs: 其他参数
        :return: (用户模型, 认证账号模型)
        :raises APIException: 当验证失败时抛出异常
        """
        pass


class PasswordRegisterStrategy:
    """密码注册策略"""

    async def validate_credentials(self, account: str, **kwargs) -> Tuple[UserModel, UserAuthAccountModel]:
        password = kwargs.get("password")
        if not password:
            raise APIException(message=t("error.auth.password_required"))

        user, auth_account = await AuthAccountManager.register_by_password(account, password)
        return user, auth_account


class EmailRegisterStrategy:
    """邮箱注册策略"""

    async def validate_credentials(self, account: str, **kwargs) -> Tuple[UserModel, UserAuthAccountModel]:
        code = kwargs.get("code")
        if not code:
            raise APIException(message=t("error.auth.code_required"))

        # 验证邮箱验证码
        if not await AuthService._verify_captcha(f"email_code:{account}", code):
            raise APIException(message=t("error.auth.invalid_code"))

        # 创建用户和邮箱认证账号
        user = await UserModel.create(username=f"email_{account}")
        auth_account = await AuthAccountManager.create_account(
            user=user, provider=AuthProviderEnum.EMAIL, identifier=account, is_verified=True, is_primary=True
        )
        return user, auth_account


class PhoneRegisterStrategy:
    """手机注册策略"""

    async def validate_credentials(self, account: str, **kwargs) -> Tuple[UserModel, UserAuthAccountModel]:
        code = kwargs.get("code")
        if not code:
            raise APIException(message=t("error.auth.code_required"))

        # 验证手机验证码
        if not await AuthService._verify_captcha(f"phone_code:{account}", code):
            raise APIException(message=t("error.auth.invalid_code"))

        # 创建用户和手机认证账号
        user = await UserModel.create(username=f"phone_{account}")
        auth_account = await AuthAccountManager.create_account(
            user=user, provider=AuthProviderEnum.PHONE, identifier=account, is_verified=True, is_primary=True
        )
        return user, auth_account


class AuthService:
    """认证服务类"""

    cache = redis_helper.redis

    # 登录相关
    MAX_LOGIN_ATTEMPTS = 5  # 最大登录尝试次数
    LOGIN_BLOCK_TIME = 1800  # 登录锁定时间：30分钟

    # 验证码相关
    CAPTCHA_EXPIRE_SECONDS = 300  # 验证码过期时间：5分钟
    CODE_LENGTH = 6  # 验证码长度

    # 缓存key前缀
    CACHE_KEY_LOGIN_ATTEMPTS = "login_attempts:{}"
    CACHE_KEY_CAPTCHA = "captcha:{}"
    CACHE_KEY_EMAIL_CODE = "email_code:{}"
    CACHE_KEY_PHONE_CODE = "phone_code:{}"

    # 验证码生成器映射
    CAPTCHA_GENERATORS = {
        CaptchaTypeIntEnum.GIF: GIFCaptchaGenerator,
        CaptchaTypeIntEnum.PNG: PNGCaptchaGenerator,
    }

    _login_strategies = {
        "password": PasswordLoginStrategy(),
        "email": EmailLoginStrategy(),
        "phone": PhoneLoginStrategy(),
    }

    _register_strategies = {
        "password": PasswordRegisterStrategy(),
        "email": EmailRegisterStrategy(),
        "phone": PhoneRegisterStrategy(),
    }

    @classmethod
    def _generate_code(cls) -> str:
        """生成数字验证码"""
        return "".join(random.choices("0123456789", k=cls.CODE_LENGTH))

    @classmethod
    async def send_email_code(cls, email: str) -> None:
        """
        发送邮箱验证码
        :param email: 邮箱地址
        :raises APIException: 当发送失败时抛出异常
        """
        code = cls._generate_code()
        key = f"email_code:{email}"

        # 存储验证码
        await cls.cache.set(key, code, ex=cls.CAPTCHA_EXPIRE_SECONDS)

        # 发送邮件
        await email_helper.send_template_email(
            subject=t("email.verification_code.subject"),
            recipients=email,
            template_name="verification_code",
            context={"code": code, "expire_minutes": cls.CAPTCHA_EXPIRE_SECONDS // 60},
        )

    @classmethod
    async def send_sms_code(cls, phone: str) -> None:
        """
        发送手机验证码
        :param phone: 手机号
        :raises APIException: 当发送失败时抛出异常
        """
        code = cls._generate_code()
        key = f"phone_code:{phone}"

        # 存储验证码
        await cls.cache.set(key, code, ex=cls.CAPTCHA_EXPIRE_SECONDS)

        # 发送短信
        await sms_helper.send_verification_code(phone, code)

    @classmethod
    async def reset_password(cls, account: str, provider: str, new_password: str, code: Optional[str] = None) -> None:
        """
        重置密码
        :param account: 账号
        :param provider: 认证方式 (email/phone)
        :param new_password: 新密码
        :param code: 验证码
        :raises APIException: 当重置失败时抛出异常
        """
        # 验证验证码
        if not code:
            raise APIException(message=t("error.auth.code_required"))

        if provider == "email":
            if not await cls._verify_captcha(f"email_code:{account}", code):
                raise APIException(message=t("error.auth.invalid_code"))
            auth_account = await UserAuthAccountModel.get_by_identifier(AuthProviderEnum.EMAIL, account)
        elif provider == "phone":
            if not await cls._verify_captcha(f"phone_code:{account}", code):
                raise APIException(message=t("error.auth.invalid_code"))
            auth_account = await UserAuthAccountModel.get_by_identifier(AuthProviderEnum.PHONE, account)
        else:
            raise APIException(message=t("error.auth.invalid_provider"))

        if not auth_account:
            raise APIException(message=t("error.auth.account_not_found"))

        # 获取或创建密码认证账号
        password_account = await UserAuthAccountModel.get_primary_account(
            auth_account.user_id, AuthProviderEnum.PASSWORD
        )
        if not password_account:
            password_account = await AuthAccountManager.create_account(
                user=auth_account.user,
                provider=AuthProviderEnum.PASSWORD,
                identifier=account,
                credential=get_password_hash(new_password),
                is_verified=True,
                is_primary=True,
            )
        else:
            password_account.credential = get_password_hash(new_password)
            await password_account.save()

    @classmethod
    async def change_password(cls, user_id: int, old_password: str, new_password: str) -> None:
        """
        修改密码
        :param user_id: 用户ID
        :param old_password: 旧密码
        :param new_password: 新密码
        :raises APIException: 修改失败时抛出异常
        """
        # 获取密码认证账号
        auth_account = await UserAuthAccountModel.get_primary_account(user_id, AuthProviderEnum.PASSWORD)
        if not auth_account:
            raise APIException(message=t("error.auth.password_not_set"))

        # 验证旧密码
        if not verify_password(old_password, auth_account.credential):
            raise APIException(message=t("error.auth.invalid_old_password"))

        # 更新密码
        auth_account.credential = get_password_hash(new_password)
        await auth_account.save()

    @classmethod
    async def bind_account(
        cls, user_id: int, provider: str, identifier: str, code: Optional[str] = None, credential: Optional[str] = None
    ) -> None:
        """
        绑定账号
        :param user_id: 用户ID
        :param provider: 认证方式
        :param identifier: 认证账号
        :param code: 验证码（当provider为email/phone时必须）
        :param credential: 认证凭证（当provider为password时必须）
        :raises APIException: 当绑定失败时抛出异常
        """
        # 检查账号是否已被绑定
        if await UserAuthAccountModel.get_by_identifier(AuthProviderEnum[provider.upper()], identifier):
            raise APIException(message=t("error.auth.account_already_bound"))

        # 根据provider选择认证方式
        if provider == "email":
            if not code:
                raise APIException(message=t("error.auth.code_required"))
            if not await cls._verify_captcha(f"email_code:{identifier}", code):
                raise APIException(message=t("error.auth.invalid_code"))
            is_verified = True

        elif provider == "phone":
            if not code:
                raise APIException(message=t("error.auth.code_required"))
            if not await cls._verify_captcha(f"phone_code:{identifier}", code):
                raise APIException(message=t("error.auth.invalid_code"))
            is_verified = True

        elif provider == "password":
            if not credential:
                raise APIException(message=t("error.auth.password_required"))
            is_verified = True

        else:
            raise APIException(message=t("error.auth.invalid_provider"))

        # 创建认证账号
        user = await UserModel.get(id=user_id)
        await AuthAccountManager.create_account(
            user=user,
            provider=AuthProviderEnum[provider.upper()],
            identifier=identifier,
            credential=get_password_hash(credential) if credential else None,
            is_verified=is_verified,
            is_primary=True,
        )

    @classmethod
    async def unbind_account(cls, user_id: int, provider: str) -> None:
        """
        解绑账号
        :param user_id: 用户ID
        :param provider: 认证方式
        :raises APIException: 当解绑失败时抛出异常
        """
        # 获���认证账号
        auth_account = await UserAuthAccountModel.get_primary_account(user_id, AuthProviderEnum[provider.upper()])
        if not auth_account:
            raise APIException(message=t("error.auth.account_not_bound"))

        # 检查是否还有其他认证方式
        other_accounts = await UserAuthAccountModel.filter(
            user_id=user_id, provider__not=AuthProviderEnum[provider.upper()]
        ).count()
        if other_accounts == 0:
            raise APIException(message=t("error.auth.last_auth_method"))

        # 删除认证账号
        await auth_account.delete()

    @classmethod
    async def _manage_login_attempts(cls, account: str, success: bool = False) -> None:
        """
        统一管理登录尝试
        :param account: 账号
        :param success: 是否登录成功
        :raises APIException: 当尝试次数超过限制时抛出异常
        """

        key = cls.CACHE_KEY_LOGIN_ATTEMPTS.format(account)

        if success:
            await cls.cache.delete(key)
            return

        attempts = await cls.cache.get(key)

        if attempts and int(attempts) >= cls.MAX_LOGIN_ATTEMPTS and not settings.DEBUG:
            raise APIException(message=t("error.auth.too_many_attempts"))
        await cls.cache.set(key, 1 if not attempts else int(attempts) + 1, ex=cls.LOGIN_BLOCK_TIME)

    @classmethod
    async def _validate_login_attempt(
        cls, account: str, captcha_id: Optional[str] = None, captcha_code: Optional[str] = None
    ) -> None:
        """验证登录尝试"""
        # 检查登录尝试次数
        await cls._manage_login_attempts(account)

        # 验证图形验证码
        if settings.ENABLE_CAPTCHA and captcha_id and captcha_code:
            if not await cls._verify_captcha(captcha_id, captcha_code):
                raise APIException(message=t("error.auth.invalid_captcha"))

    @classmethod
    async def _generate_token(cls, user: UserModel) -> Dict[str, str]:
        """
        生成访问令牌和刷新令牌
        :param user: 用户模型
        :return: 包含access_token和refresh_token的字典
        """
        access_token = create_access_token({"sub": str(user.id)})
        refresh_token = create_refresh_token({"sub": str(user.id)})
        return {"access_token": access_token, "refresh_token": refresh_token, "token_type": "bearer"}

    @classmethod
    async def _verify_captcha(cls, captcha_id: str, captcha_code: str) -> bool:
        """
        验证验证码
        :param captcha_id: 验证码ID
        :param captcha_code: 验证码
        :return: 是否验证通过
        """
        if not captcha_id or not captcha_code:
            return False

        key = f"captcha:{captcha_id}"
        stored = await cls.cache.get(key)
        if not stored:
            return False

        stored_code, stored_time = stored.split(":")
        # 检查验证码是否过期
        if int(time.time()) - int(stored_time) > cls.CAPTCHA_EXPIRE_SECONDS:
            await cls.cache.delete(key)
            return False

        # 验证码大小写不敏感
        if stored_code.lower() != captcha_code.lower():
            return False

        await cls.cache.delete(key)
        return True

    @classmethod
    async def generate_captcha(cls, captcha_type: CaptchaTypeIntEnum = CaptchaTypeIntEnum.GIF) -> Tuple[str, str]:
        """
        生成验证码
        :param captcha_type: 验证码类型 (gif/png)
        :return: (验证码ID, 验证码图片base64字符串)
        """
        generator = cls.CAPTCHA_GENERATORS.get(captcha_type)
        if not generator:
            raise ValueError(f"Unsupported captcha type: {captcha_type}")

        config = CaptchaConfig()
        image_bytes, code = await generator(config).generate()

        captcha_id = str(uuid4())
        # 存储验证码和生成时间
        await cls.cache.set(f"captcha:{captcha_id}", f"{code}:{int(time.time())}", ex=cls.CAPTCHA_EXPIRE_SECONDS)

        base64_image = b64encode(image_bytes).decode("utf-8")
        logger.info(f"captcha_id: {captcha_id}, code: {code}")
        return captcha_id, base64_image

    @classmethod
    async def login(
        cls,
        account: str,
        provider: str,
        password: Optional[str] = None,
        code: Optional[str] = None,
        captcha_id: Optional[str] = None,
        captcha_code: Optional[str] = None,
    ) -> Dict[str, str]:
        """
        用户登录
        支持多种登录方式:
        1. 密码登录
        2. 邮箱验证码登录
        3. 手机验证码登录
        4. GitHub登录（通过github_login方法）

        :param account: 账号
        :param provider: 认证方式
        :param password: 密码（当provider为password时必须）
        :param code: 验证码（当provider为email/phone时必须）
        :param captcha_id: 图形验证码ID
        :param captcha_code: 图形验证码
        :return: 令牌信息
        :raises APIException: 当登录失败时抛出异常
        """
        # 验证登录尝试
        await cls._validate_login_attempt(account, captcha_id, captcha_code)

        # 获取登录策略
        if provider not in cls._login_strategies:
            raise APIException(message=t("error.auth.invalid_provider"))

        strategy = cls._login_strategies.get(provider)

        # 验证凭证
        auth_account = await strategy.validate_credentials(account, password=password, code=code)

        # 获取用户并检查状态
        user = await auth_account.user
        if not user.is_active:
            raise APIException(message=t("error.auth.account_disabled"))

        # 登录成功，清除尝试记录
        await cls._manage_login_attempts(account, success=True)
        return await cls._generate_token(user)

    @classmethod
    async def register(
        cls,
        account: str,
        provider: str,
        password: Optional[str] = None,
        code: Optional[str] = None,
        captcha_id: Optional[str] = None,
        captcha_code: Optional[str] = None,
    ) -> Dict[str, str]:
        """
        用户注册
        支持多种注册方式:
        1. 密码注册
        2. 邮箱验证码注册
        3. 手机验证码注册

        :param account: 账号
        :param provider: 认证方式
        :param password: 密码（当provider为password时必须）
        :param code: 验证码（当provider为email/phone时必须）
        :param captcha_id: 图形验证码ID
        :param captcha_code: 图形验证码
        :return: 令牌信息
        :raises APIException: 当注册失败时抛出异常
        """
        # 验证图形验证码
        if settings.ENABLE_CAPTCHA and captcha_id and captcha_code:
            if not await cls._verify_captcha(captcha_id, captcha_code):
                raise APIException(message=t("error.auth.invalid_captcha"))

        # 获取注册策略
        if provider not in cls._register_strategies:
            raise APIException(message=t("error.auth.invalid_provider"))

        strategy = cls._register_strategies.get(provider)

        # 验证凭证并创建用户
        user, auth_account = await strategy.validate_credentials(account, password=password, code=code)

        return await cls._generate_token(user)

    @classmethod
    async def refresh_token(cls, refresh_token: str) -> Dict[str, str]:
        """
        刷新访问令牌
        :param refresh_token: 刷新令牌
        :return: 新的令牌信息
        :raises APIException: 当刷新失败时抛出异常
        """
        payload = verify_refresh_token(refresh_token)
        user_id = payload.get("sub")
        if not user_id:
            raise APIException(message=t("error.auth.invalid_token"))

        user = await UserModel.get_or_none(id=int(user_id))
        if not user or not user.is_active:
            raise APIException(message=t("error.auth.account_not_found"))

        return await cls._generate_token(user)

    @classmethod
    async def logout(cls, token: str) -> bool:
        """
        登出
        将令牌加入黑名单
        :param token: 访问令牌
        :return: 是否成功
        """
        return await add_token_to_blacklist(token)

    @classmethod
    async def github_login(cls, github_id: str, github_data: Dict[str, Any], access_token: str) -> Dict[str, str]:
        """
        GitHub登录
        支持GitHub OAuth登录，自动创建或关联用户

        :param github_id: GitHub用户ID
        :param github_data: GitHub用户数据
        :param access_token: GitHub访问令牌
        :return: 令牌信息
        :raises APIException: 当登录失败时抛出异常
        """
        try:
            user, auth_account, is_new_user = await AuthAccountManager.login_by_github(
                github_id=github_id, github_data=github_data, access_token=access_token
            )
            return await cls._generate_token(user)

        except ValueError as e:
            logger.error(f"GitHub数据验证失败: {str(e)}")
            raise APIException(message=t("error.auth.github_data_invalid"))

        except Exception as e:
            logger.error(f"GitHub登录失败: {str(e)}")
            raise APIException(message=t("error.auth.github_login_failed"))
